// Copyright 2018 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ensure

import (
	"bufio"
	"fmt"
	"io"
	"sort"
	"strings"

	"go.chromium.org/luci/common/errors"
	"go.chromium.org/luci/common/iotools"

	"go.chromium.org/luci/cipd/common"
)

// VersionsFile contains a mapping "(package name, version) -> instance ID" used
// to resolve versions (instead of backend calls) for ensure files that have
// $ResolvedVersions directive.
//
// In serialized form it is represented as a set of triples separated by one
// or more new lines (there must be no new lines inside the triple):
//
// """
// # Comments are allowed, they are skipped (not even considered as '\n').
// <package name>
// <version>
// <resolved instance ID>
//
// <package name>
// <version>
// <resolved instance ID>
// """
//
// Leading and trailing whitespace on a line is ignored.
//
// In the canonical serialization triples are ordered by (package, version).
//
// VersionsFile is safe for read-only concurrent use. Concurrent modifications
// should be protected by a lock. This is just a map in disguise.
type VersionsFile map[unresolvedVer]string

type unresolvedVer struct {
	pkg string
	ver string
}

// AddVersion adds (or overrides) an instance ID mapped to the given version.
//
// Returns an error if any of the arguments is invalid.
//
// If 'ver' is already an instance ID, just checks if it is equal to 'iid' and
// silently doesn't modify the map.
func (v VersionsFile) AddVersion(pkg, ver, iid string) error {
	if err := common.ValidatePackageName(pkg); err != nil {
		return err
	}
	if err := common.ValidateInstanceVersion(ver); err != nil {
		return err
	}
	if err := common.ValidateInstanceID(iid, common.AnyHash); err != nil {
		return err
	}

	if common.ValidateInstanceID(ver, common.AnyHash) == nil {
		if ver != iid {
			return errors.Reason(
				"version given as instance ID (%q) should resolve into that ID, not into %q",
				ver, iid).Err()
		}
		return nil
	}

	v[unresolvedVer{pkg, ver}] = iid
	return nil
}

// ResolveVersion returns a pin matching the given version or an error if such
// version is not in the map.
//
// If 'ver' is already an instance ID, returns it right away.
func (v VersionsFile) ResolveVersion(pkg, ver string) (common.Pin, error) {
	if common.ValidateInstanceID(ver, common.AnyHash) == nil {
		return common.Pin{PackageName: pkg, InstanceID: ver}, nil
	}
	if iid, ok := v[unresolvedVer{pkg, ver}]; ok {
		return common.Pin{PackageName: pkg, InstanceID: iid}, nil
	}
	return common.Pin{}, errors.Reason("not in the versions file").Err()
}

// Equal returns true if version files have same entries.
func (v VersionsFile) Equal(a VersionsFile) bool {
	if len(v) != len(a) {
		return false
	}
	for ver, iid := range v {
		if a[ver] != iid {
			return false
		}
	}
	return true
}

// Serialize writes the VersionsFile to an io.Writer in canonical order.
func (v VersionsFile) Serialize(w io.Writer) error {
	keys := make([]unresolvedVer, 0, len(v))
	for k := range v {
		keys = append(keys, k)
	}
	sort.Slice(keys, func(i, j int) bool {
		l, r := keys[i], keys[j]
		if l.pkg != r.pkg {
			return l.pkg < r.pkg
		}
		return l.ver < r.ver
	})

	_, err := iotools.WriteTracker(w, func(w io.Writer) error {
		fmt.Fprintf(w, "# This file is auto-generated by 'cipd ensure-file-resolve'.\n")
		fmt.Fprintf(w, "# Do not modify manually. All changes will be overwritten.\n")
		for _, key := range keys {
			fmt.Fprintf(w, "\n%s\n\t%s\n\t%s\n", key.pkg, key.ver, v[key])
		}
		return nil
	})
	return err
}

// ParseVersionsFile parses previously serialized versions file.
func ParseVersionsFile(r io.Reader) (VersionsFile, error) {
	res := VersionsFile{}

	lineNo := 0
	makeError := func(fmtStr string, args ...interface{}) error {
		args = append([]interface{}{lineNo}, args...)
		return fmt.Errorf("failed to parse versions file (line %d): "+fmtStr, args...)
	}

	const (
		stWaitingPkg = "a package name"
		stWaitingVer = "a package version"
		stWaitingIID = "an instance ID"
		stWaitingNL  = "a new line"
	)
	state := stWaitingPkg
	pkg := ""
	ver := ""
	iid := ""

	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		lineNo++

		line := strings.TrimSpace(scanner.Text())

		// Comments are grammatically insignificant (unlike empty lines), so skip
		// the completely.
		if len(line) > 0 && line[0] == '#' {
			continue
		}

		switch state {
		case stWaitingPkg:
			if line == "" {
				continue // can have more than one empty line between triples
			}
			pkg = line
			if err := common.ValidatePackageName(pkg); err != nil {
				return nil, makeError("%s", err)
			}
			state = stWaitingVer

		case stWaitingVer:
			if line == "" {
				return nil, makeError("expecting a version name, not a new line")
			}
			ver = line
			if err := common.ValidateInstanceVersion(ver); err != nil {
				return nil, makeError("%s", err)
			}
			state = stWaitingIID

		case stWaitingIID:
			if line == "" {
				return nil, makeError("expecting an instance ID, not a new line")
			}
			iid = line
			if err := common.ValidateInstanceID(iid, common.AnyHash); err != nil {
				return nil, makeError("%s", err)
			}
			if err := res.AddVersion(pkg, ver, iid); err != nil {
				panic(err) // impossible, everything has been validated already
			}
			pkg, ver, iid = "", "", ""
			state = stWaitingNL

		case stWaitingNL:
			if line == "" {
				state = stWaitingPkg
				continue
			}
			return nil, makeError("expecting an empty line between each version definition triple")
		}
	}

	if state != stWaitingPkg && state != stWaitingNL {
		return nil, makeError("unexpected EOF, expecting %s", state)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return res, nil
}
